package pl.softech.demo2d.physics.fluid;

import java.util.Collection;

import pl.softech.demo2d.physics.fluid.grid.Cell;
import pl.softech.demo2d.physics.fluid.grid.Cell.Boundary;
import pl.softech.demo2d.physics.fluid.grid.Cell.Flag;
import pl.softech.demo2d.physics.fluid.grid.Surface;

public class Simulation {

	private final Cell[][] cells;
	private final Collection<Particle> particles;
	private final int nx, ny;
	private final NSDiscretization nsDiscretization;

	private final double maxPressureDiff;

	public Simulation(Cell[][] cells, Collection<Particle> particles, NSDiscretization nsDiscretization,
			double maxPressureDiff) {
		this.cells = cells;
		this.particles = particles;
		this.nx = cells.length;
		this.ny = cells[0].length;
		this.nsDiscretization = nsDiscretization;
		this.maxPressureDiff = maxPressureDiff;
	}

	public Collection<Particle> getParticles() {
		return particles;
	}

	public Cell[][] getCells() {
		return cells;
	}

	public void update(double dt) {

		nsDiscretization.setDt(dt);

		flagCells();

		calculateDivergency();

		calculatePressureSource();

		calculatePressure();

		calculateVelocity();

		calculateSurfaceVelocity();

		calculateBoundaryVelocity();

		moveParticles();
	}

	private void moveParticles() {

		double dx = nsDiscretization.getDx();
		double dy = nsDiscretization.getDy();
		double dt = nsDiscretization.getDt();

		for (Particle p : particles) {
			p.move(cells, dx, dy, dt);
		}
	}

	private void calculateVelocity() {
		// TODO Narazie bez supportu dla ostrych brzegów

		for (int i = 1; i < nx - 1; i++) {
			for (int j = 1; j < ny - 1; j++) {

				// Interesuja nas tylko kompórki powierzchniowe i pełne
				if (cells[i][j].flag != Flag.FULL && cells[i][j].flag != Flag.SURFACE) {
					continue;
				}

				cells[i][j].u = nsDiscretization.u(i, j, cells);
				cells[i][j].v = nsDiscretization.v(i, j, cells);

			}
		}

	}

	private void calculateBoundaryVelocity() {
		for (int i = 1; i < nx - 1; i++) {
			for (int j = 1; j < ny - 1; j++) {

				// nie intereuja nas komórki brzegowe
				if (cells[i][i].flag == Flag.BOUNDARY) {
					continue;
				}

				// komorka brzegowa z prawej strony
				if (cells[i + 1][j].flag == Flag.BOUNDARY) {

					cells[i][j].u = 0;
					// Jesli z poslizgiem to
					cells[i + 1][j].v = cells[i][j].v;
					// a jak nie to
					if (cells[i + 1][j].boundary == Boundary.NO_SLIP) {
						cells[i + 1][j].v *= -1;
					}

				}

				// komorka brzegowa z lewej strony
				if (cells[i - 1][j].flag == Flag.BOUNDARY) {

					cells[i - 1][j].u = 0;
					cells[i - 1][j].v = cells[i][j].v;
					if (cells[i - 1][j].boundary == Boundary.NO_SLIP) {
						cells[i - 1][j].v *= -1;
					}

				}

				// komorka brzegowa u gory
				if (cells[i][j + 1].flag == Flag.BOUNDARY) {

					cells[i][j].v = 0;
					cells[i][j + 1].u = cells[i][j].u;
					if (cells[i][j + 1].boundary == Boundary.NO_SLIP) {
						cells[i][j + 1].u *= -1;
					}
				}

				// komorka brzegowa z dołu
				if (cells[i][j - 1].flag == Flag.BOUNDARY) {

					cells[i][j - 1].v = 0;
					cells[i][j - 1].u = cells[i][j].u;
					if (cells[i][j - 1].boundary == Boundary.NO_SLIP) {
						cells[i][j - 1].u *= -1;
					}
				}

			}
		}

	}

	private void calculateSurfaceVelocity() {

		double dx = nsDiscretization.getDx();
		double dy = nsDiscretization.getDy();
		double dt = nsDiscretization.getDt();
		double gx = nsDiscretization.getGx();
		double gy = nsDiscretization.getGy();

		/* Na brzegach nie potrzebujmemy obliczać */
		for (int i = 1; i < nx - 1; i++) {
			for (int j = 1; j < ny - 1; j++) {

				if (cells[i][j].flag == Flag.EMPTY) {

					if (cells[i + 1][j].flag == Flag.EMPTY) {
						cells[i][j].u = 0;
					}

					if (cells[i][j + 1].flag == Flag.EMPTY) {
						cells[i][j].v = 0;
					}

					continue;
				}

				// Intereuja nas tylko komorki powierzchniowe
				if (cells[i][j].flag != Flag.SURFACE) {
					continue;
				}

				cells[i][j].surface.adjust(i, j, cells, dx, dy, dt, gx, gy);

			}
		}

	}

	private void calculateBoundaryPressure(int i, int j) {

		double dx = nsDiscretization.getDx();
		double dy = nsDiscretization.getDy();
		double gx = nsDiscretization.getGx();
		double gy = nsDiscretization.getGy();
		double nu = nsDiscretization.getNu();

		// komorka brzegowa z prawej strony
		if (cells[i + 1][j].flag == Flag.BOUNDARY) {

			cells[i + 1][j].fi = cells[i][j].fi + gx * dx;

			if (cells[i + 1][j].boundary == Boundary.NO_SLIP) {
				cells[i + 1][j].fi += 2 * nu * (cells[i - 1][j].u - cells[i][j].u) / dx;
			}

		}

		// komorka brzegowa z lewej strony
		if (cells[i - 1][j].flag == Flag.BOUNDARY) {

			cells[i - 1][j].fi = cells[i][j].fi - gx * dx;

			if (cells[i - 1][j].boundary == Boundary.NO_SLIP) {
				cells[i - 1][j].fi += 2 * nu * (cells[i - 1][j].u - cells[i][j].u) / dx;
			}

		}

		// komorka brzegowa u gory
		if (cells[i][j + 1].flag == Flag.BOUNDARY) {

			cells[i][j + 1].fi = cells[i][j].fi + gy * dy;

			if (cells[i][j + 1].boundary == Boundary.NO_SLIP) {
				cells[i][j + 1].fi += 2 * nu * (cells[i][j - 1].v - cells[i][j].v) / dy;
			}
		}

		// komorka brzegowa z dołu
		if (cells[i][j - 1].flag == Flag.BOUNDARY) {

			cells[i][j - 1].fi = cells[i][j].fi - gy * dy;

			if (cells[i][j - 1].boundary == Boundary.NO_SLIP) {
				cells[i][j - 1].fi += 2 * nu * (cells[i][j - 1].v - cells[i][j].v) / dy;
			}
		}

	}

	private void calculatePressure() {

		double maxDiff;
		do {
			maxDiff = 0;
			for (int i = 1; i < nx - 1; i++) {
				for (int j = 1; j < ny - 1; j++) {

					// Obliczmy ciśnienie tylko dla pełnych komórek
					if (cells[i][j].flag != Flag.FULL) {

						if (cells[i][j].flag == Flag.SURFACE) {

							calculateBoundaryPressure(i, j);
						}

						continue;
					}

					double old = cells[i][j].fi;
					cells[i][j].fi = nsDiscretization.fi(i, j, cells);
					double diff = Math.abs(cells[i][j].fi - old);

					if (diff > maxDiff) {
						if (old == 0) {
							old = 1;
						}
						maxDiff = diff / Math.abs(old);
					}
				}
			}
		} while (maxDiff > maxPressureDiff);

	}

	private void calculatePressureSource() {

		for (int i = 1; i < nx - 1; i++) {
			for (int j = 1; j < ny - 1; j++) {

				if (cells[i][j].flag == Flag.FULL || cells[i][j].flag == Flag.SURFACE) {

					cells[i][j].z = nsDiscretization.z(i, j, cells);

				}

			}
		}

	}

	private void calculateDivergency() {

		for (int i = 1; i < nx - 1; i++) {
			for (int j = 1; j < ny - 1; j++) {

				if (cells[i][j].flag == Flag.FULL || cells[i][j].flag == Flag.SURFACE) {
					cells[i][j].div = nsDiscretization.D(i, j, cells);
				}

			}
		}

	}

	private void flagCells() {

		for (int i = 0; i < cells.length; i++) {
			for (int j = 0; j < cells[0].length; j++) {
				cells[i][j].un = 0;
				cells[i][j].vn = 0;
				cells[i][j].n = 0;
			}
		}

		for (Particle p : particles) {
			cells[p.i][p.j].un += p.u;
			cells[p.i][p.j].vn += p.v;
			cells[p.i][p.j].n++;
		}

		// Flagowanie komorek z powierzchniowych na puste i odwrotnie
		for (int i = 1; i < nx - 1; i++) {
			for (int j = 1; j < ny - 1; j++) {

				if (cells[i][j].flag == Flag.SURFACE) {
					if (!cells[i][j].hasParticles()) {

						cells[i][j].flag = Flag.EMPTY;

						// W ksiazce jest troche inaczej
						if (!cells[i + 1][j].hasParticles()) {
							cells[i][j].u = 0;
						}

						if (!cells[i][j + 1].hasParticles()) {
							cells[i][j].v = 0;
						}

					}
				} else if (cells[i][j].flag == Flag.EMPTY) {

					if (cells[i][j].hasParticles()) {

						cells[i][j].flag = Flag.SURFACE;

						// W ksiazce jest troche inaczej

						if (!cells[i + 1][j].hasParticles()) {
							cells[i][j].u = cells[i][j].un / cells[i][j].n;
						}

						if (!cells[i][j + 1].hasParticles()) {
							cells[i][j].v = cells[i][j].vn / cells[i][j].n;
						}

					}

				}

			}
		}

		// Flagowanie komorek z powierzchniowych na pełne i odwrotnie
		for (int i = 1; i < nx - 1; i++) {
			for (int j = 1; j < ny - 1; j++) {

				// Interesuja nas tylko komórki zawierające cząstki
				if (!cells[i][j].hasParticles()) {
					continue;
				}

				if (cells[i][j].flag == Flag.FULL) {

					// Czy jakikolwiek sąsiad jest bez cząstki ?
					
//					if (!cells[i + 1][j].hasParticles() || !cells[i - 1][j].hasParticles()
//							|| !cells[i][j + 1].hasParticles() || !cells[i][j - 1].hasParticles()) {
//						// Jeśli tak to mamy do czynienia z komórką
//						// powierzchniową.
//						cells[i][j].flag = Flag.SURFACE;
//					}
					
					// Czy jakikolwiek sąsiad jest bez cząstki ?
					if (!cells[i][j].calculateIsFull()) {
						// Jeśli tak to mamy do czynienia z komórką
						// powierzchniową.
						cells[i][j].flag = Flag.SURFACE;
					}
					

				} else if (cells[i][j].flag == Flag.SURFACE) {

					double fi = 0;
					double n = 0;

					if (cells[i + 1][j].flag == Flag.FULL) {
						fi += cells[i + 1][j].fi;
						n++;
					}

					if (cells[i - 1][j].flag == Flag.FULL) {
						fi += cells[i - 1][j].fi;
						n++;
					}

					if (cells[i][j + 1].flag == Flag.FULL) {
						fi += cells[i][j + 1].fi;
						n++;
					}

					if (cells[i][j - 1].flag == Flag.FULL) {
						fi += cells[i][j - 1].fi;
						n++;
					}

					// Średnia ciśnień z komórek sąsiadujących
					if (n != 0) {
						cells[i][j].fi = fi / n;
					}

					// Czy komórka jest pełna ?
					if (n == 4 || cells[i][j].calculateIsFull()) {
						cells[i][j].flag = Flag.FULL;
					}
				}

			}
		}

		flagSurfaceCells();
	}

	private void flagSurfaceCells() {

		for (int i = 1; i < nx - 1; i++) {
			for (int j = 1; j < ny - 1; j++) {

				// Intereuja nas tylko komorki powierzchniowe
				if (cells[i][j].flag != Flag.SURFACE) {
					continue;
				}

				cells[i][j].surface = Surface.getInstance(i, j, cells);

			}
		}

	}
}
